package com.capsulode.jdbc;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;

public class SqlInsert<T> implements SqlBuilder {

    private final StringBuilder sql;
    private final List<Object> params = new ArrayList<>();
    private final List columnValueExtractors = new ArrayList();
    private int indexOfColumnsOver = 0;

    public SqlInsert(CharSequence tableName) {
        this.sql = new StringBuilder("insert into ")
                .append(tableName)
                .append(" (");
    }

    public SqlInsert<T> column(String columnName, Function<T, Object> columnValueExtractor) {
        sql.append(columnName).append(",");
        columnValueExtractors.add(columnValueExtractor);
        return this;
    }

    public SqlInsert<T> column(String columnName, Serializable columnValue) {
        sql.append(columnName).append(",");
        columnValueExtractors.add(columnValue);
        return this;
    }

    public SqlInsert<T> column(String columnName, SqlFunction func) {
        sql.append(columnName).append(",");
        columnValueExtractors.add(func);
        return this;
    }

    public SqlInsert<T> insert(T t) {
        if (indexOfColumnsOver == 0) {
            indexOfColumnsOver = sql.length() - 1;

            sql.delete(indexOfColumnsOver, sql.length())
                    .append(") values ");
        } else {
            sql.append(",");
        }
        if (columnValueExtractors.isEmpty()) {
            throw new IllegalStateException("No column to insert!");
        }

        applyObject(t);
        sql.deleteCharAt(sql.length() - 1);
        return this;
    }

    public SqlInsert<T> batch(Collection<T> rows) {
        if (indexOfColumnsOver == 0) {
            indexOfColumnsOver = sql.length() - 1;
        }

        if (columnValueExtractors.isEmpty()) {
            throw new IllegalStateException("No column to insert!");
        }

        if (rows.isEmpty()) {
            throw new IllegalArgumentException("No data to insert!");
        }

        sql.delete(indexOfColumnsOver, sql.length())
                .append(") values ");
        rows.forEach(this::applyObject);

        sql.deleteCharAt(sql.length() - 1);
        return this;
    }

    private void applyObject(T t) {
        sql.append("(");
        columnValueExtractors.forEach(extractor -> {
            if (extractor instanceof SqlFunction) {
                SqlFunction func = (SqlFunction) extractor;
                sql.append(func.getTemplate()).append(",");
                params.addAll(Arrays.asList(func.getParams()));
                return;
            }
            sql.append("?,");
            if (extractor instanceof Function) {
                params.add(((Function) extractor).apply(t));
            } else {
                params.add(extractor);
            }
        });
        sql.deleteCharAt(sql.length() - 1).append("),");
    }

    public String toString() {
        return getSql();
    }

    public String getSql() {
        if (indexOfColumnsOver == 0) {
            // neither `insert` nor `batch` method called.
            insert(null);
        }
        return sql.toString();
    }

    public Object[] getParams() {
        if (indexOfColumnsOver == 0) {
            // neither `insert` nor `batch` method called.
            insert(null);
        }
        return params.toArray();
    }
}
